iT邦幫忙

2024 iThome 鐵人賽

DAY 14
0
自我挑戰組

我的Java自學之路:一個轉職者的30篇技術統整系列 第 14

Java進階:並行集合與原子操作

  • 分享至 

  • xImage
  •  

1. 引言

並行集合類別大多位於java.util.concurrent包中,包括ConcurrentHashMap、CopyOnWriteArrayList和各種BlockingQueue實現等。

原子操作則提供了一種無鎖的同步機制,允許在多執行緒環境中安全地更新共享變數,而無需使用顯式鎖定。
Java的java.util.concurrent.atomic包提供了一系列支援原子操作的類別,如AtomicInteger、AtomicLong等。

2. 並行集合概述

主要特點包括:

  1. 執行緒安全:並行集合內部實現了同步機制,可以安全地在多執行緒環境中使用,無需額外的同步處理。

  2. 高並行性:相比於使用synchronized關鍵字的傳統同步集合,並行集合採用更細粒度的鎖定策略或無鎖算法,允許多個執行緒同時訪問集合的不同部分。

  3. 效能優化:並行集合針對常見的多執行緒場景進行了優化,在高並發情況下通常能提供更好的效能。

Java中常見的並行集合包括:

  • ConcurrentHashMap:執行緒安全的HashMap實現
  • CopyOnWriteArrayList:適用於讀多寫少場景的ArrayList替代品
  • ConcurrentLinkedQueue:無界的執行緒安全隊列
  • BlockingQueue介面的多種實現:如ArrayBlockingQueue、LinkedBlockingQueue等
  • ConcurrentSkipListMap和ConcurrentSkipListSet:支援高並發訪問的有序映射和集合

使用並行集合的優勢:

  1. 簡化程式碼:無需手動實現複雜的同步邏輯
  2. 提高效能:特別是在高並發場景下
  3. 減少錯誤:降低了因同步問題導致的錯誤風險

然而,並行集合並非萬能的。在選擇使用時,需要考慮以下因素:

  • 應用場景:是否真的需要並行訪問?
  • 讀寫比例:某些並行集合在讀多寫少的場景下表現更佳
  • 記憶體使用:部分並行集合可能比其非並行版本使用更多記憶體

在接下來的章節中,我們將詳細介紹幾種常用的並行集合,並探討它們的特性和適用場景。

3. ConcurrentHashMap

ConcurrentHashMap是Java並行程式設計中最常用的並行集合之一,它提供了執行緒安全的Map實現,並且在高並發環境下表現出色。

特性:

  1. 分段鎖(Segmented Locking):ConcurrentHashMap內部使用多個鎖來保護不同的資料段,允許多個執行緒同時訪問Map的不同部分。
  2. 讀操作無鎖:大多數讀操作無需加鎖,提高了並行讀取的效能。
  3. 迭代器弱一致性:迭代器反映的是建立時的資料狀態,不會拋出ConcurrentModificationException。

使用方法:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);

// 原子性操作
map.putIfAbsent("key3", 3);
map.replace("key1", 1, 10);

// 併合操作
map.merge("key1", 5, Integer::sum);

// 計算操作
map.compute("key4", (k, v) -> (v == null) ? 1 : v + 1);

// 批量操作
map.forEach((k, v) -> System.out.println(k + " = " + v));

優勢:

  1. 高並發性能:在多執行緒環境下,ConcurrentHashMap通常比同步的HashMap或Hashtable效能更好。
  2. 無需額外同步:內建的執行緒安全機制使得使用者無需額外實現同步邏輯。
  3. 豐富的原子操作:提供了多種原子性的複合操作,如putIfAbsent、replace等。

使用場景:

  • 需要執行緒安全的共享緩存
  • 高並發的資料存取場景
  • 需要頻繁讀寫的共享資料結構

注意事項:

  1. 雖然單個操作是原子的,但多個操作的組合不保證原子性。
  2. 迭代器的弱一致性可能導致在迭代過程中看不到最新的修改。
  3. 記憶體使用可能比標準HashMap稍高。

ConcurrentHashMap是一個強大的並行工具,適合在多執行緒環境中替代同步的HashMap或Hashtable。正確使用ConcurrentHashMap可以顯著提升應用程式的並行處理能力和整體效能。

4. CopyOnWriteArrayList

CopyOnWriteArrayList是ArrayList的一個執行緒安全變體,專為處理讀多寫少的並發場景而設計。它的特點是在執行寫操作時,會複製整個底層陣列。

特性:

  1. 讀操作無鎖:所有的讀操作都不需要加鎖,提供了很高的並發性。
  2. 寫時複製:每次寫操作都會創建底層陣列的一個新副本,然後在新副本上進行修改。
  3. 迭代器的快照語義:迭代器反映的是它創建時的陣列快照,不會拋出ConcurrentModificationException。

使用方法:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("項目1");
list.add("項目2");

// 讀操作
String item = list.get(0);

// 寫操作
list.add("項目3");

// 迭代
for (String s : list) {
    System.out.println(s);
}

// 批量操作
list.addAll(Arrays.asList("項目4", "項目5"));

優勢:

  1. 高並發讀取:適合讀操作遠多於寫操作的場景。
  2. 迭代安全:迭代過程中,即使有其他執行緒修改了列表,也不會拋出異常。
  3. 無需額外同步:內建的執行緒安全機制使得使用者無需額外實現同步邏輯。

使用場景:

  • 事件監聽器列表
  • 讀多寫少的緩存
  • 需要執行緒安全的小型列表

注意事項:

  1. 寫操作的成本較高:每次寫操作都會複製整個陣列,對於大型列表可能會影響效能。
  2. 內存使用:由於寫操作時的複製機制,可能會暫時佔用雙倍的內存。
  3. 不適合頻繁修改的場景:如果寫操作頻繁,效能會顯著下降。

CopyOnWriteArrayList是一個在特定場景下非常有用的工具。通過犧牲寫操作的效能來換取讀操作和迭代的高效性,需要仔細評估應用場景的讀寫比例,以確保能夠帶來效能提升而不是損失。

5. BlockingQueue介面及其實現

BlockingQueue是Java並發包中的一個介面,擴展Queue介面,提供了阻塞的插入和獲取操作,非常適合用於生產者-消費者模式(Producer-Consumer Pattern)的實現。

BlockingQueue的主要特性:

  1. 阻塞操作:當隊列滿時,插入操作會被阻塞;當隊列空時,獲取操作會被阻塞。
  2. 執行緒安全:所有實現都是執行緒安全的。
  3. 支援超時:可以設置阻塞操作的超時時間。

常見的BlockingQueue實現:

  1. ArrayBlockingQueue:

    • 基於陣列的有界阻塞隊列
    • 構造時需指定容量,一旦創建容量不能改變
  2. LinkedBlockingQueue:

    • 基於鏈表的可選有界阻塞隊列
    • 可以指定容量,也可以不指定(預設為Integer.MAX_VALUE)
  3. PriorityBlockingQueue:

    • 支援優先級的無界阻塞隊列
    • 元素按照自然順序或者自定義比較器排序
  4. DelayQueue:

    • 延遲獲取元素的無界阻塞隊列
    • 元素只有在其指定的延遲時間到期後才能被取出
  5. SynchronousQueue:

    • 不儲存元素的阻塞隊列
    • 每個插入操作必須等待另一個執行緒的對應移除操作

使用範例(以ArrayBlockingQueue為例):

BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);

// 生產者
new Thread(() -> {
    try {
        queue.put("任務");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

// 消費者
new Thread(() -> {
    try {
        String task = queue.take();
        processTask(task);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}).start();

使用場景:

  • 生產者-消費者模式
  • 工作佇列
  • 資料緩衝區

選擇合適的BlockingQueue實現時,需要考慮以下因素:

  1. 容量要求:是否需要有界隊列
  2. 效能考量:陣列還是鏈表實現
  3. 排序需求:是否需要優先級排序
  4. 特殊需求:如延遲處理、同步交換等

6. ConcurrentSkipListMap和ConcurrentSkipListSet

ConcurrentSkipListMap和ConcurrentSkipListSet是Java並發包中提供的兩個重要的並行集合類別,分別實現ConcurrentNavigableMap和ConcurrentNavigableSet介面。
這兩個類別基於Skip List(跳表)資料結構實現。

特性:

  1. 有序性:元素保持自然順序或使用自定義比較器排序。
  2. 高並發性:支援多執行緒並行訪問,讀操作無需加鎖。
  3. 無鎖算法:使用CAS(Compare-and-Swap)等無鎖技術實現,避免了傳統鎖帶來的效能開銷。
  4. 期望的對數時間複雜度:大多數操作(如新增、刪除、查詢)的平均時間複雜度為O(log n)。

ConcurrentSkipListMap使用範例:

ConcurrentSkipListMap<String, Integer> map = new ConcurrentSkipListMap<>();
map.put("A", 1);
map.put("C", 3);
map.put("B", 2);

// 獲取第一個元素
Map.Entry<String, Integer> firstEntry = map.firstEntry();
System.out.println("First entry: " + firstEntry);

// 獲取小於等於指定鍵的最大鍵值對
Map.Entry<String, Integer> floorEntry = map.floorEntry("B");
System.out.println("Floor entry of 'B': " + floorEntry);

// 獲取子映射
NavigableMap<String, Integer> subMap = map.subMap("A", true, "C", false);
System.out.println("SubMap from 'A' to 'C': " + subMap);

ConcurrentSkipListSet使用範例:

ConcurrentSkipListSet<Integer> set = new ConcurrentSkipListSet<>();
set.add(3);
set.add(1);
set.add(4);
set.add(2);

// 獲取第一個元素
Integer first = set.first();
System.out.println("First element: " + first);

// 獲取大於等於指定元素的最小元素
Integer ceiling = set.ceiling(3);
System.out.println("Ceiling of 3: " + ceiling);

// 獲取子集
NavigableSet<Integer> subSet = set.subSet(2, true, 4, false);
System.out.println("SubSet from 2 to 4: " + subSet);

使用場景:

  1. 需要並行訪問的有序映射或集合
  2. 頻繁執行範圍查詢的應用
  3. 需要高效的插入、刪除和查詢操作的並發環境

優勢:

  1. 高並發性能:在多執行緒環境下表現優異
  2. 有序性:自動維護元素順序,便於範圍操作
  3. 無鎖設計:減少執行緒競爭和阻塞

注意事項:

  1. 記憶體使用:相比於非並行版本,可能會使用更多的記憶體
  2. 迭代器弱一致性:迭代過程中可能不會反映最新的修改

ConcurrentSkipListMap和ConcurrentSkipListSet為需要並行訪問的有序集合提供了高效的解決方案,使用時需要權衡有序性和並行性能的需求。

7. 原子操作與原子類別

原子操作是指不可被中斷的一個或一系列操作,在並發程式設計中,原子操作可以確保在多執行緒環境下的資料一致性。
Java提供的原子類別位於java.util.concurrent.atomic包中,使用無鎖算法來實現原子操作。

主要的原子類別包括:

  1. AtomicInteger和AtomicLong:
    用於整數和長整數的原子操作。

    範例:

    AtomicInteger counter = new AtomicInteger(0);
    int newValue = counter.incrementAndGet(); // 原子性地增加1並獲取新值
    boolean success = counter.compareAndSet(newValue, 10); // 比較並設置新值
    
  2. AtomicBoolean:
    用於布林值的原子操作。

    範例:

    AtomicBoolean flag = new AtomicBoolean(false);
    boolean oldValue = flag.getAndSet(true); // 設置新值並返回舊值
    
  3. AtomicReference:
    用於物件引用的原子操作。

    範例:

    AtomicReference<String> ref = new AtomicReference<>("初始值");
    ref.set("新值");
    String oldValue = ref.getAndUpdate(val -> val + "更新"); // 更新值並返回舊值
    

這些原子類別的主要特點:

  1. 無鎖操作:使用CAS(Compare-and-Swap)等技術實現,避免了傳統鎖的開銷。
  2. 原子性保證:所有操作都是原子的,不會被其他執行緒中斷。
  3. 可見性保證:變數的修改對所有執行緒立即可見。

使用場景:

  • 計數器
  • 標誌位
  • 緩存值
  • 並發演算法中的共享狀態

優勢:

  1. 高效能:相比於同步塊,原子操作通常有更好的效能。
  2. 簡化程式碼:無需手動實現複雜的同步邏輯。
  3. 避免死鎖:由於不使用鎖,因此不會發生死鎖問題。

注意事項:

  1. ABA問題:在某些情況下,值可能經歷了 A -> B -> A 的變化,但是原子操作可能無法檢測到這種變化。
  2. 僅適用於簡單操作:對於複雜的複合操作,可能仍需要使用同步塊或鎖。

8. 原子陣列與原子引用

Java提供專門化的原子類別,包括原子陣列和進階的原子引用類別,這些類別擴展了原子操作的應用範圍,使得在更複雜的場景中也能保證資料的一致性。

原子陣列:

  1. AtomicIntegerArray:
    用於整數陣列的原子操作。

    範例:

    AtomicIntegerArray array = new AtomicIntegerArray(10);
    array.set(0, 1);
    int oldValue = array.getAndIncrement(0); // 原子性地增加指定索引的值
    
  2. AtomicLongArray:
    用於長整數陣列的原子操作。

  3. AtomicReferenceArray:
    用於物件引用陣列的原子操作。

進階原子引用類別:

  1. AtomicStampedReference:
    帶有版本戳記的原子引用,用於解決ABA問題。

    範例:

    AtomicStampedReference<String> ref = new AtomicStampedReference<>("初始值", 0);
    int[] stampHolder = new int[1];
    String value = ref.get(stampHolder);
    boolean success = ref.compareAndSet(value, "新值", stampHolder[0], stampHolder[0] + 1);
    
  2. AtomicMarkableReference:
    帶有標記的原子引用,可用於實現邏輯刪除等操作。

    範例:

    AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("初始值", false);
    boolean[] markHolder = new boolean[1];
    String value = ref.get(markHolder);
    boolean success = ref.compareAndSet(value, "新值", markHolder[0], true);
    

使用場景:

  • 原子陣列:適用於需要並發訪問的共享陣列,如計數器陣列、狀態陣列等。
  • AtomicStampedReference:適用於需要避免ABA問題的場景,如並發緩存。
  • AtomicMarkableReference:適用於需要額外布林標記的場景,如並發資料結構中的邏輯刪除。

優勢:

  1. 擴展了原子操作的應用範圍,使得更複雜的資料結構也能享受到無鎖並發的好處。
  2. 提供了解決特定並發問題(如ABA問題)的專門工具。
  3. 保持了原子類別的高效能特性。

注意事項:

  1. 使用這些進階類別時,需要仔細考慮並發邏輯,確保正確處理版本或標記。
  2. 原子陣列操作雖然是原子的,但多個操作的組合可能不是原子的,需要額外注意。

9. 實踐與效能考量

  1. 選擇適當的並行集合:

    • 根據應用場景選擇合適的並行集合。例如,讀多寫少的場景可以考慮CopyOnWriteArrayList。
    • 對於需要排序的並行集合,考慮使用ConcurrentSkipListMap或ConcurrentSkipListSet。
  2. 合理使用原子類別:

    • 對於簡單的計數器或標誌,優先使用AtomicInteger或AtomicBoolean。
    • 對於複雜的原子操作,考慮使用AtomicReference配合自定義的更新函數。
  3. 避免過度同步:

    • 使用並行集合時,避免在外部再加鎖,這可能會導致效能下降。
    • 利用並行集合提供的原子操作方法,如putIfAbsent、computeIfAbsent等。
  4. 注意伸縮性:

    • 對於ConcurrentHashMap,考慮預設設置合適的初始容量和負載因子,以減少重新調整大小的次數。
    • 對於BlockingQueue,根據實際需求選擇有界或無界隊列。
  5. 正確處理迭代:

    • 記住並行集合的迭代器通常是弱一致性的,可能不反映最新的修改。
    • 在迭代過程中,避免對集合進行結構性修改。
  6. 合理使用批量操作:

    • 利用並行集合提供的批量操作方法,如forEach、reduceValues等,這些方法通常比手動迭代更高效。
  7. 注意記憶體使用:

    • 並行集合可能比其非並行版本使用更多的記憶體,在記憶體受限的環境中要謹慎使用。
  8. 正確處理異常:

    • 在使用阻塞方法(如BlockingQueue的put和take)時,要正確處理InterruptedException。
  9. 效能測試和調優:

    • 在實際負載下進行效能測試,比較不同並行集合和實現策略的效果。
    • 使用Java的並行性能分析工具,如Java Flight Recorder,來識別潛在的效能問題。
  10. 避免過度細化:

    • 過度細化的並行操作可能導致效能下降。確保並行化的收益大於其開銷。
  11. 注意ABA問題:

    • 在使用CAS操作時,考慮是否需要使用AtomicStampedReference來避免ABA問題。
  12. 正確使用執行緒池:

    • 配合並行集合使用執行緒池,可以更好地控制並行度和資源使用。

本篇文章同步刊載: JYI.TW
筆者個人的網站: JUNYI


上一篇
Java進階:執行緒池與執行器框架
系列文
我的Java自學之路:一個轉職者的30篇技術統整14
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言